package de.mmis.core.base.util;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;

import de.mmis.core.base.DeserializableAnnotations.ConstructorDeserializable;
import de.mmis.core.base.DeserializableAnnotations.DeserializationConstructor;
import de.mmis.core.base.MethodDescriptor.P;
import de.mmis.core.base.event.AbstractObservable;
import de.mmis.core.base.event.AbstractTypedEvent;
import de.mmis.core.base.gui.AbstractTableControlComponent;
import de.mmis.core.base.util.NotifyMap.MapEvent;
import de.mmis.core.base.util.NotifyMap.MapEvent.Type;

/**
 * Map extended by the function to notify listeners on change.<br>
 * This map is also thread-safe.
 * 
 * @author Martin Dyrba
 * @param <K>
 *            the key type
 * @param <V>
 *            the value type
 * @see Map
 */
@ConstructorDeserializable(typeIdentifier = "notify-map")
public class NotifyMap<K, V> extends AbstractObservable<MapEvent> implements
		Map<K, V> {

	/** the events fired by this map */
	@ConstructorDeserializable(typeIdentifier = "notify-map-event")
	public static class MapEvent extends AbstractTypedEvent<MapEvent.Type> {

		/** the event type */
		public enum Type {
			/** an entry of the map has been changed */
			MapEntryChanged,
			/** the map has been cleared */
			MapCleared,
			/** an entry of the map has been removed */
			MapEntryRemoved,
			/** an entry of the map has been added */
			MapEntryAdded
		}

		protected Object key;

		/**
		 * Constructor.
		 * 
		 * @param type
		 *            the event type
		 * @param key
		 *            the key object that has been changed, or <code>null</code>
		 *            if the map has been cleared
		 */
		@DeserializationConstructor
		public MapEvent(@P("EventType") Type type, @P("Key") Object key) {
			super(type);
			this.key = key;
		}

		/**
		 * Gets the key of the changed or added entry.
		 * 
		 * @return the key object or <code>null</code> if the map has been
		 *         cleared
		 */
		public Object getKey() {
			return key;
		}
	}

	/**
	 * (View-only) control component for notify maps.
	 * 
	 * @author Martin Dyrba
	 */
	public static class NotifyMapControlComponent extends
			AbstractTableControlComponent<NotifyMap<?, ?>, MapEvent> {

		public NotifyMapControlComponent() {
			super("Map View", NotifyMap.class, new Vector<String>());

			colNames.add("Key");
			colNames.add("Value");
		}

		@Override
		protected Vector<Vector<Object>> updateData(MapEvent event) {
			Vector<Vector<Object>> newData = new Vector<Vector<Object>>();
			if (getDevice().tryEnsureAvailability()) {
				// access to entrySet() is already synchronized
				for (Entry<?, ?> entry : getDevice().getDevice().entrySet()) {
					Vector<Object> row = new Vector<Object>();
					row.add(entry.getKey());
					row.add(entry.getValue());
					newData.add(row);
				}
				getDevice().releaseAvailability();
			}
			return newData;
		}

		@Override
		protected Vector<Vector<Object>> initializeData() {
			return updateData(null);
		}
	}

	protected Map<K, V> map;

	/**
	 * Constructor.
	 * 
	 * @param map
	 *            the map object to use.
	 */
	@DeserializationConstructor
	public NotifyMap(@P("Map") Map<K, V> map) {
		this.map = Collections.synchronizedMap(map);
	}

	@Override
	public int size() {
		return map.size();
	}

	@Override
	public boolean isEmpty() {
		return map.isEmpty();
	}

	@Override
	public boolean containsKey(Object key) {
		return map.containsKey(key);
	}

	@Override
	public boolean containsValue(Object value) {
		return map.containsValue(value);
	}

	@Override
	public V get(Object key) {
		return map.get(key);
	}

	@Override
	public V put(K key, V value) {
		V val = map.put(key, value);
		if (val == null)
			fireEvent(new MapEvent(Type.MapEntryAdded, key));
		else
			fireEvent(new MapEvent(Type.MapEntryChanged, key));
		return val;
	}

	@Override
	public V remove(Object key) {
		V res = map.remove(key);
		if (res != null)
			fireEvent(new MapEvent(Type.MapEntryRemoved, key));
		return res;
	}

	@Override
	public void putAll(Map<? extends K, ? extends V> m) {
		// fires an event for each entry in given list
		List<K> addedList = new ArrayList<K>();
		List<K> replacedList = new ArrayList<K>();
		synchronized (m) {
			for (java.util.Map.Entry<? extends K, ? extends V> entry : m
					.entrySet()) {
				V val = map.put(entry.getKey(), entry.getValue());
				if (val == null)
					addedList.add(entry.getKey());
				else
					replacedList.add(entry.getKey());
			}
		}
		for (K k : addedList) {
			fireEvent(new MapEvent(Type.MapEntryAdded, k));
		}
		for (K k : replacedList) {
			fireEvent(new MapEvent(Type.MapEntryChanged, k));
		}
	}

	@Override
	public void clear() {
		map.clear();
		fireEvent(new MapEvent(Type.MapCleared, null));
	}

	/** @see Collections#synchronizedMap(Map) */
	@Override
	public Set<K> keySet() {
		return map.keySet();
	}

	/** @see Collections#synchronizedMap(Map) */
	@Override
	public Collection<V> values() {
		return map.values();
	}

	/** @see Collections#synchronizedMap(Map) */
	@Override
	public Set<java.util.Map.Entry<K, V>> entrySet() {
		return map.entrySet();
	}

	/**
	 * Returns the original map wrapped by this class.<br>
	 * <b>Note:</b> The returned map should never be used to modify the map!
	 * This method is intended to be used for (de)serialization purposes only!
	 * 
	 * @return the original map wrapped by this class
	 */
	public Map<K, V> getMap() {
		return map;
	}
}
